/*
 * Copyright 2007 ZXing authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.zxing.result;

import com.google.zxing.Result;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * <p>Abstract class representing the result of decoding a barcode, as more than
 * a String -- as some type of structured data. This might be a subclass which represents
 * a URL, or an e-mail address. {@link #parseResult(Result)} will turn a raw
 * decoded string into the most appropriate type of structured representation.</p>
 *
 * <p>Thanks to Jeff Griffin for proposing rewrite of these classes that relies less
 * on exception-based mechanisms during parsing.</p>
 *
 * @author Sean Owen
 */
public abstract class ResultParser {
    
    private static final ResultParser[] PARSERS = {
        new URIResultParser(),
        new URLTOResultParser()
    };
    
    private static final Pattern DIGITS = Pattern.compile("\\d+");
    private static final Pattern AMPERSAND = Pattern.compile("&");
    private static final Pattern EQUALS = Pattern.compile("=");
    private static final String BYTE_ORDER_MARK = "\ufeff";
    
    static final String[] EMPTY_STR_ARRAY = new String[0];
    
    /**
     * Attempts to parse the raw {@link Result}'s contents as a particular type
     * of information (email, URL, etc.) and return a {@link ParsedResult} encapsulating
     * the result of parsing.
     *
     * @param theResult the raw {@link Result} to parse
     * @return {@link ParsedResult} encapsulating the parsing result
     */
    public abstract ParsedResult parse(Result theResult);
    
    protected static String getMassagedText(Result result) {
        String text = result.getText();
        if (text.startsWith(BYTE_ORDER_MARK)) {
            text = text.substring(1);
        }
        return text;
    }
    
    public static ParsedResult parseResult(Result theResult) {
        for (ResultParser parser : PARSERS) {
            ParsedResult result = parser.parse(theResult);
            if (result != null) {
                return result;
            }
        }
        return new TextParsedResult(theResult.getText(), null);
    }
    
    protected static void maybeAppend(String value, StringBuilder result) {
        if (value != null) {
            result.append('\n');
            result.append(value);
        }
    }
    
    protected static void maybeAppend(String[] value, StringBuilder result) {
        if (value != null) {
            for (String s : value) {
                result.append('\n');
                result.append(s);
            }
        }
    }
    
    protected static String[] maybeWrap(String value) {
        return value == null ? null : new String[]{value};
    }
    
    protected static String unescapeBackslash(String escaped) {
        int backslash = escaped.indexOf('\\');
        if (backslash < 0) {
            return escaped;
        }
        int max = escaped.length();
        StringBuilder unescaped = new StringBuilder(max - 1);
        unescaped.append(escaped.toCharArray(), 0, backslash);
        boolean nextIsEscaped = false;
        for (int i = backslash; i < max; i++) {
            char c = escaped.charAt(i);
            if (nextIsEscaped || c != '\\') {
                unescaped.append(c);
                nextIsEscaped = false;
            } else {
                nextIsEscaped = true;
            }
        }
        return unescaped.toString();
    }
    
    protected static int parseHexDigit(char c) {
        if (c >= '0' && c <= '9') {
            return c - '0';
        }
        if (c >= 'a' && c <= 'f') {
            return 10 + (c - 'a');
        }
        if (c >= 'A' && c <= 'F') {
            return 10 + (c - 'A');
        }
        return -1;
    }
    
    protected static boolean isStringOfDigits(CharSequence value, int length) {
        return value != null && length > 0 && length == value.length() && DIGITS.matcher(value).matches();
    }
    
    protected static boolean isSubstringOfDigits(CharSequence value, int offset, int length) {
        if (value == null || length <= 0) {
            return false;
        }
        int max = offset + length;
        return value.length() >= max && DIGITS.matcher(value.subSequence(offset, max)).matches();
    }
    
    static Map<String, String> parseNameValuePairs(String uri) {
        int paramStart = uri.indexOf('?');
        if (paramStart < 0) {
            return null;
        }
        Map<String, String> result = new HashMap<>(3);
        for (String keyValue : AMPERSAND.split(uri.substring(paramStart + 1))) {
            appendKeyValue(keyValue, result);
        }
        return result;
    }
    
    private static void appendKeyValue(CharSequence keyValue, Map<String, String> result) {
        String[] keyValueTokens = EQUALS.split(keyValue, 2);
        if (keyValueTokens.length == 2) {
            String key = keyValueTokens[0];
            String value = keyValueTokens[1];
            try {
                value = urlDecode(value);
                result.put(key, value);
            } catch (IllegalArgumentException iae) {
                // continue; invalid data such as an escape like %0t
            }
        }
    }
    
    static String urlDecode(String encoded) {
        try {
            return URLDecoder.decode(encoded, "UTF-8");
        } catch (UnsupportedEncodingException uee) {
            throw new IllegalStateException(uee); // can't happen
        }
    }
    
    static String[] matchPrefixedField(String prefix, String rawText, char endChar, boolean trim) {
        List<String> matches = null;
        int i = 0;
        int max = rawText.length();
        while (i < max) {
            i = rawText.indexOf(prefix, i);
            if (i < 0) {
                break;
            }
            i += prefix.length(); // Skip past this prefix we found to start
            int start = i; // Found the start of a match here
            boolean more = true;
            while (more) {
                i = rawText.indexOf(endChar, i);
                if (i < 0) {
                    // No terminating end character? uh, done. Set i such that loop terminates and break
                    i = rawText.length();
                    more = false;
                } else if (countPrecedingBackslashes(rawText, i) % 2 != 0) {
                    // semicolon was escaped (odd count of preceding backslashes) so continue
                    i++;
                } else {
                    // found a match
                    if (matches == null) {
                        matches = new ArrayList<>(3); // lazy init
                    }
                    String element = unescapeBackslash(rawText.substring(start, i));
                    if (trim) {
                        element = element.trim();
                    }
                    if (!element.isEmpty()) {
                        matches.add(element);
                    }
                    i++;
                    more = false;
                }
            }
        }
        if (matches == null || matches.isEmpty()) {
            return null;
        }
        return matches.toArray(EMPTY_STR_ARRAY);
    }
    
    private static int countPrecedingBackslashes(CharSequence s, int pos) {
        int count = 0;
        for (int i = pos - 1; i >= 0; i--) {
            if (s.charAt(i) == '\\') {
                count++;
            } else {
                break;
            }
        }
        return count;
    }
    
    static String matchSinglePrefixedField(String prefix, String rawText, char endChar, boolean trim) {
        String[] matches = matchPrefixedField(prefix, rawText, endChar, trim);
        return matches == null ? null : matches[0];
    }
    
}
